﻿@page "/textOnPath"
@using VectSharp;
@using VectSharp.SVG;

<style>
	table {
		table-layout: fixed;
	}

	td {
		width: 200px;
		padding: 10px;
	}
</style>

<div style="width: 100vw; height: 100vh; position: relative;">
	<div style="width: calc(100% - 400px); height: 100%; position: absolute; top: 0; left: 0; text-align: center">
		<img src="@imgSource" style="max-width: 100%; max-height: 100%; margin-top: 50vh; transform: translate(0, -50%)" />
	</div>

	<div style="width: 400px; height: 100vh; position: absolute; top: 0; right: 0;">
		<table style="margin-top: 50vh; transform: translate(0, -50%)">

			<tr>
				<td colspan="2" style="width: 400px; text-align: center; padding-top: 0">
					<MatTextField @bind-Value="@text" OnInput="(e) => text = e.Value.ToString()" Label="Text"></MatTextField>
				</td>
			</tr>
			<tr>
				<td>
					<div style="display: inline-block; position: relative; width: 380px">
						<span class="mdc-floating-label mdc-floating-label--float-above" style="margin-left:0.5em; margin-top: 2em; color: rgba(0, 0, 0, 0.6)">Reference</span><br />
						<div style="position: relative">
							<div style="display: inline-block; width: 300px">
								<MatSlider ValueMin="0" ValueMax="100" @bind-Value="@reference" Immediate="true"></MatSlider>
							</div>
							<span style="position: absolute; top: 50%; left: 320px; transform: translate(0, -50%)">@((reference / 100).ToString("0.00", System.Globalization.CultureInfo.InvariantCulture))</span>
						</div>
					</div>
				</td>
			</tr>
			<tr>
				<td colspan="2" style="width: 400px; text-align: center; padding-top: 0">
					<div style="display: inline-block; position: relative">
						<span class="mdc-floating-label mdc-floating-label--float-above" style="margin-left:0.5em; margin-top: 2em; color: rgba(0, 0, 0, 0.6)">Anchor</span><br />
						<MatRadioGroup @bind-Value="@anchor" TValue="int">
							<MatRadioButton Value="0" TValue="int">Left</MatRadioButton>
							<MatRadioButton Value="1" TValue="int">Center</MatRadioButton>
							<MatRadioButton Value="2" TValue="int">Right</MatRadioButton>
						</MatRadioGroup>
					</div>
				</td>
			</tr>
			<tr>
				<td colspan="2" style="width: 400px; text-align: center; padding-top: 0">
					<div style="display: inline-block; position: relative">
						<span class="mdc-floating-label mdc-floating-label--float-above" style="margin-left:0.5em; margin-top: 2em; color: rgba(0, 0, 0, 0.6)">Baseline</span><br />
						<MatRadioGroup @bind-Value="@baseline" TValue="int">
							<MatRadioButton Value="0" TValue="int">Top</MatRadioButton>
							<MatRadioButton Value="2" TValue="int">Middle</MatRadioButton>
							<MatRadioButton Value="3" TValue="int">Baseline</MatRadioButton>
							<MatRadioButton Value="1" TValue="int">Bottom</MatRadioButton>
						</MatRadioGroup>
					</div>
				</td>
			</tr>
		</table>
	</div>
</div>

@code {

	private double _reference = 50;
	private double reference
	{
		get
		{
			return _reference;
		}
		set
		{
			_reference = value;
			Render();
		}
	}

	private int _anchor = 1;
	private int anchor
	{
		get
		{
			return _anchor;
		}

		set
		{
			_anchor = value;
			Render();
		}
	}

	private int _baseline = 3;
	private int baseline
	{
		get
		{
			return _baseline;
		}

		set
		{
			_baseline = value;
			Render();
		}
	}


	private string _text = "VectSharp";
	private string text
	{
		get
		{
			return _text;
		}
		set
		{
			_text = value;
			Render();
		}
	}


	private string imgSource = "";

	protected override Task OnInitializedAsync()
	{
		CreateCache();

		Render();
		return Task.CompletedTask;
	}

	GraphicsPath path = new GraphicsPath().MoveTo(20, 30).CubicBezierTo(50, 20, 80, 40, 80, 70);
	double pathLength;

	Point[] points;
	Point[] normals;

	private void CreateCache()
	{
		points = path.Linearise(5).GetPoints().First().ToArray();
		normals = path.GetLinearisationPointsNormals(5).First().ToArray();
		pathLength = path.MeasureLength();
	}

	private Point GetPointAt(double position)
	{
		if (position < 0)
		{
			return new Point(points[0].X + normals[0].Y * position * pathLength, points[0].Y - normals[0].X * position * pathLength);
		}
		else if (position > 1)
		{
			return new Point(points[^1].X + normals[^1].Y * (position - 1) * pathLength, points[^1].Y - normals[^1].X * (position - 1) * pathLength);
		}

		int indLeft = (int)Math.Floor(position * (points.Length - 1));
		int indRight = (int)Math.Ceiling(position * (points.Length - 1));

		if (indLeft == indRight)
		{
			return points[indLeft];
		}
		else
		{
			double t = position * (points.Length - 1) - indLeft;

			return new Point(points[indLeft].X * (1 - t) + points[indRight].X * t, points[indLeft].Y * (1 - t) + points[indRight].Y * t);
		}
	}

	private Point GetNormalAt(double position)
	{
		if (position < 0)
		{
			return normals[0];
		}
		else if (position > 1)
		{
			return normals[^1];
		}

		int indLeft = (int)Math.Floor(position * (normals.Length - 1));
		int indRight = (int)Math.Ceiling(position * (normals.Length - 1));

		if (indLeft == indRight)
		{
			return normals[indLeft];
		}
		else
		{
			double t = position * (normals.Length - 1) - indLeft;

			Point tbr = new Point(normals[indLeft].X * (1 - t) + normals[indRight].X * t, normals[indLeft].Y * (1 - t) + normals[indRight].Y * t);
			double modulus = tbr.Modulus();

			return new Point(tbr.X / modulus, tbr.Y / modulus);
		}
	}

	private void DrawAnchor(Graphics gpr, Point centre)
	{
		gpr.Save();
		gpr.Translate(centre);
		gpr.Scale(0.5, 0.5);

		Colour col = Colour.FromRgb(180, 180, 180);
		double lineThickness = 1.5;

		gpr.StrokePath(new GraphicsPath().Arc(0, -5, 2, 0, 2 * Math.PI).Close().MoveTo(0, -3).LineTo(0, 8).MoveTo(-3, 0).LineTo(3, 0), col, lineThickness, lineCap: LineCaps.Round);
		gpr.StrokePath(new GraphicsPath().Arc(0, 0, 8, Math.PI / 4 - 0.1, Math.PI / 4 * 3 + 0.1), col, lineThickness);

		double arrowSize = 3;

		gpr.FillPath(new GraphicsPath().MoveTo(8 * Math.Cos(Math.PI / 4) + arrowSize, 8 * Math.Sin(Math.PI / 4) - arrowSize).LineTo(8 * Math.Cos(Math.PI / 4) + arrowSize - arrowSize * 0.25, 8 * Math.Sin(Math.PI / 4) + arrowSize - arrowSize * 0.25).LineTo(8 * Math.Cos(Math.PI / 4) - arrowSize + arrowSize * 0.25, 8 * Math.Sin(Math.PI / 4) - arrowSize + arrowSize * 0.25).Close(), col);		

		gpr.FillPath(new GraphicsPath().MoveTo(-8 * Math.Cos(Math.PI / 4) - arrowSize, 8 * Math.Sin(Math.PI / 4) - arrowSize).LineTo(-8 * Math.Cos(Math.PI / 4) - arrowSize + arrowSize * 0.25, 8 * Math.Sin(Math.PI / 4) + arrowSize - arrowSize * 0.25).LineTo(-8 * Math.Cos(Math.PI / 4) + arrowSize - arrowSize * 0.25, 8 * Math.Sin(Math.PI / 4) - arrowSize + arrowSize * 0.25).Close(), col);

		gpr.Restore();
	}

	public void Render()
	{
		Page page = new Page(100, 100);
		Graphics graphics = page.Graphics;

		FontFamily family = FontFamily.ResolveFontFamily(FontFamily.StandardFontFamilies.Helvetica);
		Font font = new Font(family, 12);

		graphics.StrokePath(path, Colour.FromRgb(180, 180, 180), lineDash: new LineDash(5, 5, 2.5));

		Point anchorPoint = GetPointAt(reference / 100.0);
		Point normal = GetNormalAt(reference / 100.0);
		Point tangent = new Point(normal.Y, -normal.X);

		double anchorSize = 5;

		if (baseline == 0)
		{
			anchorSize *= -1;
		}

		Point anchorCentre = new Point(anchorPoint.X + normal.X * anchorSize * 2.5, anchorPoint.Y + normal.Y * anchorSize * 2.5);

		graphics.FillPath(new GraphicsPath().MoveTo(anchorPoint).LineTo(anchorPoint.X - anchorSize * tangent.X * 0.5 + anchorSize * normal.X, anchorPoint.Y - anchorSize * tangent.Y * 0.5 + anchorSize * normal.Y).LineTo(anchorPoint.X + anchorSize * tangent.X * 0.5 + anchorSize * normal.X, anchorPoint.Y + anchorSize * tangent.Y * 0.5 + anchorSize * normal.Y).Close(), Colour.FromRgb(180, 180, 180));

		DrawAnchor(graphics, anchorCentre);

		FillTextOnPath(graphics, path, text, font, Colours.Black, reference: reference / 100.0, anchor: (TextAnchors)anchor, textBaseline: (TextBaselines)baseline);

		using (MemoryStream ms = new MemoryStream())
		{
			page.SaveAsSVG(ms);
			ms.Seek(0, SeekOrigin.Begin);

			using (StreamReader sr = new StreamReader(ms))
			{
				this.imgSource = "data:image/svg+xml;base64," + Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(sr.ReadToEnd()));
			}
		}
	}


	public void FillTextOnPath(Graphics gpr, GraphicsPath path, string text, Font font, Brush fillColour, double reference = 0, TextAnchors anchor = TextAnchors.Left, TextBaselines textBaseline = TextBaselines.Top, string tag = null)
        {
            if (font.Underline != null)
            {
                font = new Font(font.FontFamily, font.FontSize, false);
            }

            double currDelta = 0;
            double pathLength = path.MeasureLength();

            Font.DetailedFontMetrics fullMetrics = font.MeasureTextAdvanced(text);

            switch (anchor)
            {
                case TextAnchors.Left:
                    break;
                case TextAnchors.Center:
                    currDelta = -fullMetrics.Width * 0.5 / pathLength;
                    break;
                case TextAnchors.Right:
                    currDelta = -fullMetrics.Width / pathLength;
                    break;
            }

            Point currentGlyphPlacementDelta = new Point();
            Point currentGlyphAdvanceDelta = new Point();
            Point nextGlyphPlacementDelta = new Point();
            Point nextGlyphAdvanceDelta = new Point();

            for (int i = 0; i < text.Length; i++)
            {
                string c = text.Substring(i, 1);

                if (Font.EnableKerning && i < text.Length - 1)
                {
                    currentGlyphPlacementDelta = nextGlyphPlacementDelta;
                    currentGlyphAdvanceDelta = nextGlyphAdvanceDelta;
                    nextGlyphAdvanceDelta = new Point();
                    nextGlyphPlacementDelta = new Point();

                    TrueTypeFile.PairKerning kerning = font.FontFamily.TrueTypeFile.Get1000EmKerning(text[i], text[i + 1]);

                    if (kerning != null)
                    {
                        currentGlyphPlacementDelta = new Point(currentGlyphPlacementDelta.X + kerning.Glyph1Placement.X, currentGlyphPlacementDelta.Y + kerning.Glyph1Placement.Y);
                        currentGlyphAdvanceDelta = new Point(currentGlyphAdvanceDelta.X + kerning.Glyph1Advance.X, currentGlyphAdvanceDelta.Y + kerning.Glyph1Advance.Y);

                        nextGlyphPlacementDelta = new Point(nextGlyphPlacementDelta.X + kerning.Glyph2Placement.X, nextGlyphPlacementDelta.Y + kerning.Glyph2Placement.Y);
                        nextGlyphAdvanceDelta = new Point(nextGlyphAdvanceDelta.X + kerning.Glyph2Advance.X, nextGlyphAdvanceDelta.Y + kerning.Glyph2Advance.Y);
                    }
                }

                Font.DetailedFontMetrics metrics = font.MeasureTextAdvanced(c);

                //Point origin = path.GetPointAtRelative(reference + currDelta + currentGlyphPlacementDelta.X * font.FontSize / 1000);

                //Point tangent = path.GetTangentAtRelative(reference + currDelta + currentGlyphPlacementDelta.X * font.FontSize / 1000 + (metrics.Width + metrics.RightSideBearing + metrics.LeftSideBearing) / pathLength * 0.5);

				Point origin = GetPointAt(reference + currDelta + currentGlyphPlacementDelta.X * font.FontSize / 1000);
				Point tangent = GetNormalAt(reference + currDelta + currentGlyphPlacementDelta.X * font.FontSize / 1000 + (metrics.Width + metrics.RightSideBearing + metrics.LeftSideBearing) / pathLength * 0.5);

                origin = new Point(origin.X - tangent.Y * currentGlyphPlacementDelta.Y * font.FontSize / 1000, origin.Y + tangent.X * currentGlyphPlacementDelta.Y * font.FontSize / 1000);

                gpr.Save();

                gpr.Translate(origin);
                gpr.Rotate(Math.Atan2(-tangent.X, tangent.Y));

                switch (textBaseline)
                {
                    case TextBaselines.Top:
                        if (i > 0)
                        {
                            gpr.FillText(new Point(metrics.LeftSideBearing, fullMetrics.Top), c, font, fillColour, textBaseline: TextBaselines.Baseline, tag);
                        }
                        else
                        {
                            gpr.FillText(new Point(0, fullMetrics.Top), c, font, fillColour, textBaseline: TextBaselines.Baseline, tag);
                        }
                        break;
                    case TextBaselines.Baseline:
                        if (i > 0)
                        {
                            gpr.FillText(new Point(metrics.LeftSideBearing, 0), c, font, fillColour, textBaseline: TextBaselines.Baseline, tag);
                        }
                        else
                        {
                            gpr.FillText(new Point(0, 0), c, font, fillColour, textBaseline: TextBaselines.Baseline, tag);
                        }
                        break;
                    case TextBaselines.Bottom:
                        if (i > 0)
                        {
                            gpr.FillText(new Point(metrics.LeftSideBearing, fullMetrics.Bottom), c, font, fillColour, textBaseline: TextBaselines.Baseline, tag);
                        }
                        else
                        {
                            gpr.FillText(new Point(0, fullMetrics.Bottom), c, font, fillColour, textBaseline: TextBaselines.Baseline, tag);
                        }
                        break;
                    case TextBaselines.Middle:
                        if (i > 0)
                        {
                            gpr.FillText(new Point(metrics.LeftSideBearing, fullMetrics.Bottom + fullMetrics.Height / 2), c, font, fillColour, textBaseline: TextBaselines.Baseline, tag);
                        }
                        else
                        {
                            gpr.FillText(new Point(0, fullMetrics.Bottom + fullMetrics.Height / 2), c, font, fillColour, textBaseline: TextBaselines.Baseline, tag);
                        }
                        break;
                }

                gpr.Restore();

                if (i > 0)
                {
                    currDelta += (metrics.Width + metrics.RightSideBearing + metrics.LeftSideBearing + currentGlyphAdvanceDelta.X * font.FontSize / 1000) / pathLength;
                }
                else
                {
                    currDelta += (metrics.Width + metrics.RightSideBearing + currentGlyphAdvanceDelta.X * font.FontSize / 1000) / pathLength;
                }
            }
        }
}
